module builder

import os
import v.pref
import v.cflag

#flag windows -l shell32
#flag windows -l dbghelp
#flag windows -l advapi32

struct MsvcResult {
	full_cl_exe_path    string
	exe_path            string
	um_lib_path         string
	ucrt_lib_path       string
	vs_lib_path         string
	um_include_path     string
	ucrt_include_path   string
	vs_include_path     string
	shared_include_path string
}

// shell32 for RegOpenKeyExW etc
// Mimics a HKEY
type RegKey voidptr

// Taken from the windows SDK
const (
	hkey_local_machine     = RegKey(0x80000002)
	key_query_value        = (0x0001)
	key_wow64_32key        = (0x0200)
	key_enumerate_sub_keys = (0x0008)
)

// Given a root key look for one of the subkeys in 'versions' and get the path
fn find_windows_kit_internal(key RegKey, versions []string) ?string {
	$if windows {
		unsafe {
			for version in versions {
				required_bytes := 0 // TODO mut
				result := C.RegQueryValueEx(key, version.to_wide(), 0, 0, 0, &required_bytes)
				length := required_bytes / 2
				if result != 0 {
					continue
				}
				alloc_length := (required_bytes + 2)
				mut value := &u16(malloc(alloc_length))
				if isnil(value) {
					continue
				}
				//
				else{}
				result2 := C.RegQueryValueEx(key, version.to_wide(), 0, 0, value, &alloc_length)
				if result2 != 0 {
					continue
				}
				// We might need to manually null terminate this thing
				// So just make sure that we do that
				if value[length - 1] != u16(0) {
					value[length] = u16(0)
				}
				res := string_from_wide(value)
				return res
			}
		}
	}
	return error('windows kit not found')
}

struct WindowsKit {
	um_lib_path         string
	ucrt_lib_path       string
	um_include_path     string
	ucrt_include_path   string
	shared_include_path string
}

// Try and find the root key for installed windows kits
fn find_windows_kit_root(host_arch string) ?WindowsKit {
	$if windows {
		root_key := RegKey(0)
		path := 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots'
		rc := C.RegOpenKeyEx(hkey_local_machine, path.to_wide(), 0, key_query_value | key_wow64_32key |
			key_enumerate_sub_keys, &root_key)
		defer {
			C.RegCloseKey(root_key)
		}
		if rc != 0 {
			return error('Unable to open root key')
		}
		// Try and find win10 kit
		kit_root := find_windows_kit_internal(root_key, ['KitsRoot10', 'KitsRoot81']) or {
			return error('Unable to find a windows kit')
		}
		kit_lib := kit_root + 'Lib'
		// println(kit_lib)
		files := os.ls(kit_lib) or {
			panic(err)
		}
		mut highest_path := ''
		mut highest_int := 0
		for f in files {
			no_dot := f.replace('.', '')
			v_int := no_dot.int()
			if v_int > highest_int {
				highest_int = v_int
				highest_path = f
			}
		}
		kit_lib_highest := kit_lib + '\\$highest_path'
		kit_include_highest := kit_lib_highest.replace('Lib', 'Include')
		// println('$kit_lib_highest $kit_include_highest')
		return WindowsKit{
			um_lib_path: kit_lib_highest + '\\um\\$host_arch'
			ucrt_lib_path: kit_lib_highest + '\\ucrt\\$host_arch'
			um_include_path: kit_include_highest + '\\um'
			ucrt_include_path: kit_include_highest + '\\ucrt'
			shared_include_path: kit_include_highest + '\\shared'
		}
	}
	return error('Host OS does not support funding a windows kit')
}

struct VsInstallation {
	include_path string
	lib_path     string
	exe_path     string
}

fn find_vs(vswhere_dir, host_arch string) ?VsInstallation {
	$if !windows {
		return error('Host OS does not support finding a Vs installation')
	}
	// Emily:
	// VSWhere is guaranteed to be installed at this location now
	// If its not there then end user needs to update their visual studio
	// installation!
	res := os.exec('"$vswhere_dir\\Microsoft Visual Studio\\Installer\\vswhere.exe" -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath') or {
		return error(err)
	}
	// println('res: "$res"')
	version := os.read_file('$res.output\\VC\\Auxiliary\\Build\\Microsoft.VCToolsVersion.default.txt') or {
		println('Unable to find msvc version')
		return error('Unable to find vs installation')
	}
	version2 := version // TODO remove. cgen option bug if expr
	// println('version: $version')
	v := if version.ends_with('\n') { version2[..version.len - 2] } else { version2 }
	lib_path := '$res.output\\VC\\Tools\\MSVC\\$v\\lib\\$host_arch'
	include_path := '$res.output\\VC\\Tools\\MSVC\\$v\\include'
	if os.exists('$lib_path\\vcruntime.lib') {
		p := '$res.output\\VC\\Tools\\MSVC\\$v\\bin\\Host$host_arch\\$host_arch'
		// println('$lib_path $include_path')
		return VsInstallation{
			exe_path: p
			lib_path: lib_path
			include_path: include_path
		}
	}
	println('Unable to find vs installation (attempted to use lib path "$lib_path")')
	return error('Unable to find vs exe folder')
}

fn find_msvc() ?MsvcResult {
	$if windows {
		processor_architecture := os.getenv('PROCESSOR_ARCHITECTURE')
		vswhere_dir := if processor_architecture == 'x86' { '%ProgramFiles%' } else { '%ProgramFiles(x86)%' }
		host_arch := if processor_architecture == 'x86' { 'X86' } else { 'X64' }
		wk := find_windows_kit_root(host_arch) or {
			return error('Unable to find windows sdk')
		}
		vs := find_vs(vswhere_dir, host_arch) or {
			return error('Unable to find visual studio')
		}
		return MsvcResult{
			full_cl_exe_path: os.real_path(vs.exe_path + os.path_separator + 'cl.exe')
			exe_path: vs.exe_path
			um_lib_path: wk.um_lib_path
			ucrt_lib_path: wk.ucrt_lib_path
			vs_lib_path: vs.lib_path
			um_include_path: wk.um_include_path
			ucrt_include_path: wk.ucrt_include_path
			vs_include_path: vs.include_path
			shared_include_path: wk.shared_include_path
		}
	} $else {
		verror('Cannot find msvc on this OS')
		return error('msvc not found')
	}
}

pub fn (mut v Builder) cc_msvc() {
	r := find_msvc() or {
		// TODO: code reuse
		if !v.pref.keep_c && v.out_name_c != 'v.c' && v.out_name_c != 'v_macos.c' {
			os.rm(v.out_name_c)
		}
		verror('Cannot find MSVC on this OS')
		return
	}
	out_name_obj := os.real_path(v.out_name_c + '.obj')
	// Default arguments
	// volatile:ms enables atomic volatile (gcc _Atomic)
	// -w: no warnings
	// 2 unicode defines
	// /Fo sets the object file name - needed so we can clean up after ourselves properly
	mut a := ['-w', '/we4013', '/volatile:ms', '/Fo"$out_name_obj"']
	if v.pref.is_prod {
		a << '/O2'
		a << '/MD'
		a << '/Zi'
		a << '/DNDEBUG'
	} else {
		a << '/Zi'
		a << '/MDd'
	}
	if v.pref.is_shared {
		if !v.pref.out_name.ends_with('.dll') {
			v.pref.out_name += '.dll'
		}
		// Build dll
		a << '/LD'
	} else if !v.pref.out_name.ends_with('.exe') {
		v.pref.out_name += '.exe'
	}
	v.pref.out_name = os.real_path(v.pref.out_name)
	// alibs := []string{} // builtin.o os.o http.o etc
	if v.pref.build_mode == .build_module {
		// Compile only
		a << '/c'
	} else if v.pref.build_mode == .default_mode {
		/*
		b := os.real_path( '${pref.default_module_path}/vlib/builtin.obj' )
		alibs << '"$b"'
		if !os.exists(b) {
			println('`builtin.obj` not found')
			exit(1)
		}
		for imp in v.table.imports {
			if imp == 'webview' {
				continue
			}
			alibs << '"' + os.real_path( '${pref.default_module_path}/vlib/${imp}.obj' ) + '"'
		}
		*/
	}
	if v.pref.sanitize {
		println('Sanitize not supported on msvc.')
	}
	// The C file we are compiling
	// a << '"$TmpPath/$v.out_name_c"'
	a << '"' + os.real_path(v.out_name_c) + '"'
	// Emily:
	// Not all of these are needed (but the compiler should discard them if they are not used)
	// these are the defaults used by msbuild and visual studio
	mut real_libs := ['kernel32.lib', 'user32.lib', 'advapi32.lib']
	sflags := v.get_os_cflags().msvc_string_flags()
	real_libs << sflags.real_libs
	inc_paths := sflags.inc_paths
	lib_paths := sflags.lib_paths
	other_flags := sflags.other_flags
	// Include the base paths
	a << '-I "$r.ucrt_include_path"'
	a << '-I "$r.vs_include_path"'
	a << '-I "$r.um_include_path"'
	a << '-I "$r.shared_include_path"'
	a << inc_paths
	a << other_flags
	// Libs are passed to cl.exe which passes them to the linker
	a << real_libs.join(' ')
	a << '/link'
	a << '/NOLOGO'
	a << '/OUT:"$v.pref.out_name"'
	a << '/LIBPATH:"$r.ucrt_lib_path"'
	a << '/LIBPATH:"$r.um_lib_path"'
	a << '/LIBPATH:"$r.vs_lib_path"'
	a << '/DEBUG:FULL' // required for prod builds to generate PDB
	if v.pref.is_prod {
		a << '/INCREMENTAL:NO' // Disable incremental linking
		a << '/OPT:REF'
		a << '/OPT:ICF'
	}
	a << lib_paths
	args := a.join(' ')
	cmd := '"$r.full_cl_exe_path" $args'
	// It is hard to see it at first, but the quotes above ARE balanced :-| ...
	// Also the double quotes at the start ARE needed.
	if v.pref.is_verbose {
		println('\n========== cl cmd line:')
		println(cmd)
		println('==========\n')
	}
	// println('$cmd')
	res := os.exec(cmd) or {
		println(err)
		verror('msvc error')
		return
	}
	if res.exit_code != 0 {
		verror(res.output)
	}
	// println(res)
	// println('C OUTPUT:')
	if !v.pref.keep_c && v.out_name_c != 'v.c' && v.out_name_c != 'v_macos.c' {
		os.rm(v.out_name_c)
	}
	// Always remove the object file - it is completely unnecessary
	os.rm(out_name_obj)
}

fn build_thirdparty_obj_file_with_msvc(path string, moduleflags []cflag.CFlag) {
	msvc := find_msvc() or {
		println('Could not find visual studio')
		return
	}
	// msvc expects .obj not .o
	mut obj_path := '${path}bj'
	obj_path = os.real_path(obj_path)
	if os.exists(obj_path) {
		println('$obj_path already built.')
		return
	}
	println('$obj_path not found, building it (with msvc)...')
	parent := os.dir(obj_path)
	files := os.ls(parent) or {
		panic(err)
	}
	mut cfiles := ''
	for file in files {
		if file.ends_with('.c') {
			cfiles += '"' + os.real_path(parent + os.path_separator + file) + '" '
		}
	}
	include_string := '-I "$msvc.ucrt_include_path" -I "$msvc.vs_include_path" -I "$msvc.um_include_path" -I "$msvc.shared_include_path"'
	// println('cfiles: $cfiles')
	btarget := moduleflags.c_options_before_target_msvc()
	atarget := moduleflags.c_options_after_target_msvc()
	cmd := '"$msvc.full_cl_exe_path" /volatile:ms /Zi /DNDEBUG $include_string /c $btarget $cfiles $atarget /Fo"$obj_path"'
	// NB: the quotes above ARE balanced.
	println('thirdparty cmd line: $cmd')
	res := os.exec(cmd) or {
		println('msvc: failed thirdparty object build cmd: $cmd')
		verror(err)
		return
	}
	if res.exit_code != 0 {
		println('msvc: failed thirdparty object build cmd: $cmd')
		verror(res.output)
		return
	}
	println(res.output)
}

struct MsvcStringFlags {
mut:
	real_libs   []string
	inc_paths   []string
	lib_paths   []string
	other_flags []string
}

fn (cflags []cflag.CFlag) msvc_string_flags() MsvcStringFlags {
	mut real_libs := []string{}
	mut inc_paths := []string{}
	mut lib_paths := []string{}
	mut other_flags := []string{}
	for flag in cflags {
		// println('fl: $flag.name | flag arg: $flag.value')
		// We need to see if the flag contains -l
		// -l isnt recognised and these libs will be passed straight to the linker
		// by the compiler
		if flag.name == '-l' {
			if flag.value.ends_with('.dll') {
				verror('MSVC cannot link against a dll (`#flag -l $flag.value`)')
			}
			// MSVC has no method of linking against a .dll
			// TODO: we should look for .defs aswell
			lib_lib := flag.value + '.lib'
			real_libs << lib_lib
		} else if flag.name == '-I' {
			inc_paths << flag.format()
		} else if flag.name == '-L' {
			lib_paths << flag.value
			lib_paths << flag.value + os.path_separator + 'msvc'
			// The above allows putting msvc specific .lib files in a subfolder msvc/ ,
			// where gcc will NOT find them, but cl will do...
			// NB: gcc is smart enough to not need .lib files at all in most cases, the .dll is enough.
			// When both a msvc .lib file and .dll file are present in the same folder,
			// as for example for glfw3, compilation with gcc would fail.
		} else if flag.value.ends_with('.o') {
			// msvc expects .obj not .o
			other_flags << '"${flag.value}bj"'
		} else {
			other_flags << flag.value
		}
	}
	mut lpaths := []string{}
	for l in lib_paths {
		lpaths << '/LIBPATH:"' + os.real_path(l) + '"'
	}
	return MsvcStringFlags{
		real_libs: real_libs
		inc_paths: inc_paths
		lib_paths: lpaths
		other_flags: other_flags
	}
}
